﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Diagnostics;

using Microsoft.Practices.Prism.Events;
using Hockey.Shared;
using System.Windows.Threading;
using System.Windows.Media.Effects;

namespace Hockey.UILayer.Controls
{
    /// <summary>
    /// Control displaying the board
    /// </summary>
    public class BoardControl : Control
    {
        /// <summary>
        /// Drawing and layout constants
        /// </summary>
        const int _margin = 10;
        const double _fieldSpacingFactor = 3.3;
        const double _fieldMargin = 20;
        const double _bondThicknessFactor = 3;
        private const double _headerRatioY = 1d / 3d;
        private const double _headerRatioX = 1d / 3d;

        
        /// <summary>
        /// Animation constants and variables
        /// </summary>      
        private const double _puckMovementDuration = 700d;
        private const double _puckRotationDuration = 600d;
        private double _puckCenterX;
        private double _puckCenterY;
        private bool _puckAnimationRunning;
        private double _puckAngle;

        /// <summary>
        /// Brushes and pens for rendering
        /// </summary>
        private LinearGradientBrush bkBrush;
        private Brush brushUnvisited;
        private Brush brushVisited;
        private Pen penUnvisited;
        private Pen penVisited;
        private LinearGradientBrush puckBrush;
        private Pen puckPen;

        /// <summary>
        /// Animation variables
        /// </summary>
        private RenderTargetBitmap _capturedBoard;
        private Stopwatch _stopWatch;
        private double _finalX, _finalY;
        private double _origX, _origY;

        /// <summary>
        /// Board reference
        /// </summary>
        private IBoard _board;

        private IEventAggregator _events;

       
        static BoardControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(BoardControl), new FrameworkPropertyMetadata(typeof(BoardControl)));
        }

        /// <summary>
        /// Constructor
        /// </summary>
        public BoardControl(IBoard board, IEventAggregator events)
        {
            if (board == null)
                throw new ArgumentNullException("board");
            if (events == null)
                throw new ArgumentNullException("Events");

            _board = board;
            _events = events;

            _stopWatch = new Stopwatch();
            _puckAngle = 0d;

            // create shadow effect
            Effect = new DropShadowEffect();

            // create brushes and pens
            CreatePensAndBrushes();

            // register events
            _events.GetEvent<BoardChanged>().Subscribe((obj) => { InvalidateVisual(); }, true);
            _events.GetEvent<AnimatePuckMovement>().Subscribe((dir) =>
                {
                    // needed check when handler recieved from keyboard input
                    if (_board.CanPlayInThisDirection(dir))
                    {
                        // get starting and ending coordinate
                        Coordinate start = _board.PuckNode.Coordinate;
                        Coordinate end = _board.PuckNode.Bonds[dir].GetNextNode(_board.PuckNode).Coordinate;

                        AnimatePuckMovement(start, end);
                    }
                }, true);
            _events.GetEvent<AnimatePuckRotation>().Subscribe((obj) =>
                {
                    AnimatePuckRotation();
                }, true);
        }

        /// <summary>
        /// Creates pens and brushes for board rendering
        /// </summary>
        private void CreatePensAndBrushes()
        {
            // render background for the drawing region
            bkBrush = new LinearGradientBrush();
            bkBrush.StartPoint = new Point(0, 0);
            bkBrush.EndPoint = new Point(1, 1);
            bkBrush.GradientStops.Add(new GradientStop(Colors.LightBlue, 0));
            bkBrush.GradientStops.Add(new GradientStop(Colors.Blue, 1));
            bkBrush.Freeze();

            // create pen and brush for rendering fields
            // TO-DO: temporarily remove radial brushes and add solid color brushes
            // for better performance on low-end machines

            //brushUnvisited = new RadialGradientBrush();
            //brushUnvisited.GradientStops.Add(new GradientStop(Colors.Yellow, 0));
            //brushUnvisited.GradientStops.Add(new GradientStop(Colors.Orange, 1));
            brushUnvisited = new SolidColorBrush(Color.FromRgb(213,222,37));
            brushUnvisited.Freeze();

            //brushVisited = new RadialGradientBrush();
            //brushVisited.GradientStops.Add(new GradientStop(Colors.Red, 0));
            //brushVisited.GradientStops.Add(new GradientStop(Colors.Orange, 0.9));

            brushVisited = new SolidColorBrush(Colors.DarkOrange);
            brushVisited.Freeze();

            penUnvisited = new Pen(Brushes.Black, 1);
            penVisited = new Pen(Brushes.Red, 2);
            penUnvisited.Freeze();
            penVisited.Freeze();

            // brush and pen for puck rendering
            puckBrush = new LinearGradientBrush();
            puckBrush.GradientStops.Add(new GradientStop(Colors.LightGray, 0));
            puckBrush.GradientStops.Add(new GradientStop(Colors.Black, 1));
            puckBrush.Freeze();

            puckPen = new Pen(Brushes.Black, 1);
            puckPen.Freeze();
        }

        protected override Size MeasureOverride(Size constraint)
        {
            Rect calcSize = CalculateRegion(constraint);

            return calcSize.Size;
        }




        #region Rendering Methods

        /// <summary>
        /// Rendering method
        /// </summary>
        protected override void OnRender(DrawingContext drawingContext)
        {
            // check if animation running, then draw board from bitmap
            if (_puckAnimationRunning)
            {
                // draw background - floors the width and height of rendered bitmap to remove image flickering 
                // when higher screen DPIs are set
                drawingContext.DrawImage(_capturedBoard, new Rect(0, 0, Math.Floor(RenderSize.Width), Math.Floor(RenderSize.Height)));

                // render animated puck
                RenderPuck(drawingContext, CalcRenderRegion(), _board.PuckNode.Coordinate);
            }
            else
            {
                // do rendering
                RenderBoard(drawingContext, false);
            }
        }

        /// <summary>
        /// Renders the board
        /// </summary>
        protected void RenderBoard(DrawingContext drawingContext, bool skipPuck)
        {
            // render transparent background
            drawingContext.DrawRectangle(Brushes.Transparent, new Pen(Brushes.Transparent, 1), new Rect(new Point(0, 0), RenderSize));

            Rect renderRegion = CalcRenderRegion();
            drawingContext.DrawRoundedRectangle(bkBrush, new Pen(Brushes.Black, 3), renderRegion, 20, 20);

            // render bonds
            RenderBonds(drawingContext, renderRegion);

            // render fields
            RenderFields(drawingContext, renderRegion);

            // render puck
            if(skipPuck == false)
                RenderPuck(drawingContext, renderRegion, _board.PuckNode.Coordinate);
        }


        /// <summary>
        /// Renders the bonds
        /// </summary>
        protected void RenderBonds(DrawingContext dc, Rect renderRegion)
        {
            // visited links dynamic cache - must be drawn later
            List<Point> visitedStartPoint = new List<Point>();
            List<Point> visitedEndPoint = new List<Point>();
            List<ePuckDir> visitedByPlayer = new List<ePuckDir>();

            // calculate radius for drawing pen
            double radius = 0;
            double x1, y1;
            CalculateFieldCenter(new Coordinate(0,0), renderRegion, out x1, out y1, out radius);

            // TO-DO: refactor to the class scope, change by resizing
            Pen drawPen = new Pen(new SolidColorBrush(Color.FromRgb(213, 222, 37)), radius / _bondThicknessFactor);
            drawPen.Freeze();

            Pen visitedPen = new Pen(Brushes.Red, radius / _bondThicknessFactor);
            visitedPen.Freeze();

            Pen visitedPen2 = new Pen(Brushes.Pink, radius / _bondThicknessFactor);
            visitedPen2.Freeze();

            // iterate across the board
            for (int row = 0; row < _board.Height; row++)
            {
                for (int col = 0; col < _board.Width; col++)
                {
                    Coordinate coor = new Coordinate(row, col);
                    INode node1 = _board.Get(coor);
                    if (node1 != null)
                    {
                        // calculate field one center
                        CalculateFieldCenter(coor, renderRegion, out x1, out y1, out radius);

                        // iterate all bonds
                        foreach (var bondPair in node1.Bonds)
                        {
                            // filter out duplicite bonds
                            if (bondPair.Key == eMoveDirection.North || bondPair.Key == eMoveDirection.East ||
                                bondPair.Key == eMoveDirection.NorthEast || bondPair.Key == eMoveDirection.NorthWest)
                                continue;

                            IBond bond = bondPair.Value;

                            // get the opposite node
                            INode node2 = bond.GetNextNode(node1);

                            // calculate field two center
                            double x2, y2;
                            CalculateFieldCenter(node2.Coordinate, renderRegion, out x2, out y2, out radius);

                            // if visited, put to the cache
                            if (bond.HasBeenVisited)
                            {
                                visitedStartPoint.Add(new Point(x1, y1));
                                visitedEndPoint.Add(new Point(x2, y2));
                                visitedByPlayer.Add(bond.VisitedByPlayer.Value);
                            }
                            else // else draw link
                            {
                                // draw the line
                                dc.DrawLine(drawPen, new Point(x1, y1), new Point(x2, y2));
                            }
                        }
                    }
                }
            }

            // draw visited links
            for (int i = 0; i < visitedStartPoint.Count; i++)
            {
                // get points
                var startP = visitedStartPoint[i];
                var endP = visitedEndPoint[i];
                var player = visitedByPlayer[i];

                //  draw line
                dc.DrawLine(player == ePuckDir.Up ? visitedPen : visitedPen2, startP, endP);
            }
        }

        protected void RenderFields(DrawingContext dc, Rect renderRegion)
        {
            // iterate across the board
            for (int row = 0; row < _board.Height; row++)
            {
                for (int col = 0; col < _board.Width; col++)
                {
                    Coordinate coor = new Coordinate(row, col);
                    INode node = _board.Get(coor);
                    
                    // if node exists
                    if (node != null)
                    {
                        // select brush and pen
                        Brush drawBrush = node.HasBeenVisited ? brushVisited : brushUnvisited;
                        Pen drawPen = node.HasBeenVisited ?  penVisited : penUnvisited;

                        // render the field
                        RenderField(dc, coor, drawPen, drawBrush, renderRegion);
                    }
                } // for
            } // for
        }

        protected void RenderPuck(DrawingContext dc, Rect renderRegion, Coordinate coor)
        {
            // calculate center of field
            double x, y, radius;
            CalculateFieldCenter(coor, renderRegion, out x, out y, out radius);

            // check puck animation
            if (_puckAnimationRunning)
            {
                x = _puckCenterX;
                y = _puckCenterY;
            }

            // draw dot
            dc.DrawEllipse(puckBrush, puckPen, new Point(x, y), radius, radius);

            // check arrow rendering
            RenderPuckArrow(dc, coor, _board.PuckDirection, renderRegion);
        }

        
        /// <summary>
        /// Renders the puck arrow
        /// </summary>
        protected void RenderPuckArrow(DrawingContext dc, Coordinate coor, ePuckDir dir, Rect renderRegion)
        {
            // calculate center of field
            double x, y, radius;
            CalculateFieldCenter(coor, renderRegion, out x, out y, out radius);

            // check puck animation running
            if (_puckAnimationRunning)
            {
                x = _puckCenterX;
                y = _puckCenterY;
            }

            // if down, push rotation transform
            if (dir == ePuckDir.Down)
            {
                dc.PushTransform(new RotateTransform(180 + _puckAngle, x, y));
            }
            else
            {
                dc.PushTransform(new RotateTransform(_puckAngle, x, y));
            }

            // create field rect
            double left = x - radius;
            double top = y - radius;
            Rect fieldRect = new Rect(left, top, radius * 2, radius * 2);
            fieldRect.Inflate(-3d, -3d);

            // create arrow geometry

            // add top point
            List<PathSegment> arrowSegments = new List<PathSegment>();

            double middleX = fieldRect.Left + fieldRect.Width / 2;
            Point startPoint = new Point(middleX, fieldRect.Top);

            // add right header point
            double headerY = fieldRect.Height * _headerRatioY;
            arrowSegments.Add(new LineSegment(new Point(middleX + headerY, fieldRect.Top + headerY), false));

            // add bottom header point on the right
            double bottomHeaderSize = (headerY * 2) * _headerRatioX;
            double rightX = middleX + headerY - bottomHeaderSize;
            arrowSegments.Add(new LineSegment(new Point(rightX, fieldRect.Top + headerY), false));

            // add points for arrow column
            arrowSegments.Add(new LineSegment(new Point(rightX, fieldRect.Top + fieldRect.Height), false));
            double leftX = rightX - bottomHeaderSize;
            arrowSegments.Add(new LineSegment(new Point(leftX, fieldRect.Top + fieldRect.Height), false));
            arrowSegments.Add(new LineSegment(new Point(leftX, fieldRect.Top + headerY), false));

            // add final left header point
            arrowSegments.Add(new LineSegment(new Point(leftX - bottomHeaderSize, fieldRect.Top + headerY), false));         

            // create path figure
            List<PathFigure> arrowFigure = new List<PathFigure>();
            arrowFigure.Add(new PathFigure(startPoint, arrowSegments, true));

            // create geometry
            PathGeometry arrowGeometry = new PathGeometry(arrowFigure);

            // do drawing
            dc.DrawGeometry(Brushes.White, new Pen(), arrowGeometry);


            // remove transformation if necessary
            dc.Pop();

        }

        /// <summary>
        /// Renders the field
        /// </summary>
        protected void RenderField(DrawingContext dc, Coordinate coor, Pen pen, Brush brush, Rect renderRegion)        
        {
            // calculate center of field
            double x, y, radius;
            CalculateFieldCenter(coor, renderRegion, out x, out y, out radius);

            // draw dot
            dc.DrawEllipse(brush, pen, new Point(x, y), radius, radius);
        }

        /// <summary>
        /// Calculates field's center and radius
        /// </summary>
        protected void CalculateFieldCenter(Coordinate coor, Rect renderRegion, out double x, out double y, out double radius)
        {
            // count up field margin
            double width = renderRegion.Width - 2 * _fieldMargin;
            double height = renderRegion.Height - 2 * _fieldMargin;

            x = renderRegion.X + _fieldMargin;
            y = renderRegion.Y + _fieldMargin;

            // calculate radius
            double countW = (_board.Width - 1) * _fieldSpacingFactor + 2;
            double countH = (_board.Height - 1) * _fieldSpacingFactor + 2;
            radius = width / countW;
            double radiusH = height / countH;
            double spaceW = radius * _fieldSpacingFactor;
            double spaceH = radiusH * _fieldSpacingFactor;

            // calculate the center of dot
            x += spaceW * coor.Col + radius;
            y += spaceH * coor.Row + radius;
        }

        #endregion

        #region Animation Methods

        private void RenderBoardToBitmap()
        {
            // get system dpi - possibly not needed
            //Matrix m = PresentationSource.FromVisual(Application.Current.MainWindow).CompositionTarget.TransformToDevice;
            //double dx = m.M11 * 96;
            //double dy = m.M22 * 96;

            // create capture board - render standard DPI 96 - WPF possibly converts DPI automatically
            // when the bitmap is rendered
            _capturedBoard = new RenderTargetBitmap((int)RenderSize.Width, (int)RenderSize.Height, 96, 96, PixelFormats.Pbgra32);

            // do board rendering
            DrawingVisual dv = new DrawingVisual();
            using (DrawingContext ctx = dv.RenderOpen())
            {
                RenderBoard(ctx, true);                
            }

            // write to bitmap
            _capturedBoard.Render(dv);        
        }

        /// <summary>
        /// Starts the puck movement animation
        /// </summary>
        private void AnimatePuckMovement(Coordinate start, Coordinate end)
        {
            // save board to bitmap
            RenderBoardToBitmap();

            // get render region
            Rect renderRegion = CalcRenderRegion();

            // calculate points
            double radius;
            CalculateFieldCenter(start, renderRegion, out _puckCenterX, out _puckCenterY, out radius);
            _origX = _puckCenterX;
            _origY = _puckCenterY;

            _puckAngle = 0d;

            CalculateFieldCenter(end, renderRegion, out _finalX, out _finalY, out radius);

            _puckAnimationRunning = true;
            _stopWatch.Reset();
            _stopWatch.Start();

            // init timer and start it
            CompositionTarget.Rendering += AnimationMovement_Tick;

            InvalidateVisual();
        }

        private void AnimationMovement_Tick(object sender, EventArgs e)
        {
            // check the frame
            if (_stopWatch.ElapsedMilliseconds > _puckMovementDuration)
            {
                _puckAnimationRunning = false;
                _stopWatch.Stop();

                // clean up timer
                CompositionTarget.Rendering -= AnimationMovement_Tick;

                // free captured board
                _capturedBoard = null;

                // notify animation has finished
                _events.GetEvent<MoveAnimationFinished>().Publish(null);
            }

            // move the puck
            double factor = ((double)_stopWatch.ElapsedMilliseconds) / ((double)_puckMovementDuration);
            _puckCenterX = _origX + (_finalX - _origX) * factor;
            _puckCenterY = _origY + (_finalY - _origY) * factor;

            InvalidateVisual();
        }

        /// <summary>
        /// Starts the puck movement animation
        /// </summary>
        private void AnimatePuckRotation()
        {
            // save board to bitmap
            RenderBoardToBitmap();

            // get render region
            Rect renderRegion = CalcRenderRegion();

            // calculate points
            double radius;
            CalculateFieldCenter(_board.PuckNode.Coordinate, renderRegion, out _puckCenterX, out _puckCenterY, out radius);
            _origX = _puckCenterX;
            _origY = _puckCenterY;

            _puckAnimationRunning = true;
            _puckAngle = 180d;

            _stopWatch.Reset();
            _stopWatch.Start();

            // init timer and start it
            CompositionTarget.Rendering += AnimationRotation_Tick;


            InvalidateVisual();
        }

        void AnimationRotation_Tick(object sender, EventArgs e)
        {
            // check the frame
            if (_stopWatch.ElapsedMilliseconds > _puckRotationDuration)
            {
                _stopWatch.Stop();

                // clean up timer
                CompositionTarget.Rendering -= AnimationRotation_Tick;

                // free captured board
                _puckAnimationRunning = false;
                _capturedBoard = null;
                _puckAngle = 0d;

                // notify animation has finished
                _events.GetEvent<RotationAnimationFinished>().Publish(null);

                InvalidateVisual();

                return;
            }

            // rotate the puck
            double factor = ((double)_stopWatch.ElapsedMilliseconds) / ((double)_puckRotationDuration);
            _puckAngle = 180d - (180d * factor);

            // notify redraw
            InvalidateVisual();
        }


        #endregion

        #region Calculation methods

        private Rect CalculateRegion(Size size)
        {
            double width = _board.Width;
            double height = _board.Height;

            if (size.Width > size.Height)
            {
                // width - height
                // x      - R.Height 
                width = width * size.Height / height;
                height = size.Height;
            }
            else
            {
                // height - width
                // x      - R.Width
                height = height * size.Width / width;
                width = size.Width;
            }

            // check out of bounds
            double factor = 1;
            if (width > size.Width)
                factor = size.Width / width;
            else if (height > size.Height)
                factor = size.Height / height;

            width *= factor;
            height *= factor;

            // count up margin
            width -= 2 * _margin;
            height -= 2 * _margin;

            // center the render region
            double x = (size.Width - width) / 2;
            double y = (size.Height - height) / 2;

            return new Rect(x, y, width, height);
        }

        /// <summary>
        /// Calculates the drawing region according to the gameboard ratio
        /// </summary>
        private Rect CalcRenderRegion()
        {
            return CalculateRegion(RenderSize);
        }
        #endregion
    }
}
